# run
# python ./demo.py > result.txt

from typing import List
from random import randint
from numpy.random import zipf
import threading
import time
import numpy as np
TIME_SCALE = 10

SLEEP_INTERVAL = 0.001*TIME_SCALE
IN_CACHE_TIME = 0.02*TIME_SCALE
OUT_CACHE_TIME = 0.04*TIME_SCALE
NORMAL_SEND_INTERVAL = 0.003*TIME_SCALE
EVIL_SEND_INTERVAL = 0.0001*TIME_SCALE

GLOBAL_START = time.time()
SIMULATION_TIME = 5

ZF_A = 1.5 # zipf的参数

def output(*args, **kwargs):
    print(*args, **kwargs)
output = lambda x:None

record = []



class Packet():
    def __init__(self, content_id, consumer_id, consumer_type):
        self.content_id = content_id
        self.consumer_id = consumer_id
        self.consumer_type = consumer_type
        self.send_timestamp = time.time()
        self.recv_timestamp = GLOBAL_START
        self.elapsed_time = 0
        self.in_cache = -1
    def set_node(self, node):
        self.node = node
    def set_recv_timestamp(self):
        self.recv_timestamp = time.time()
        self.elapsed_time = self.recv_timestamp - self.send_timestamp
    def set_cache_states(self, in_cache:bool):
        self.in_cache = in_cache
        # self.elapsed_time = 0.02 if self.in_cache else 0.04
    def __str__(self):
        return f"[{self.content_id}, {self.consumer_id}, {self.consumer_type}, {self.in_cache}, "\
               f"{self.send_timestamp-GLOBAL_START}, {self.recv_timestamp-GLOBAL_START}, {self.elapsed_time/TIME_SCALE}]"


class Link():
    def __init__(self, capcity:int):
        self.capcity = capcity
        self.used = 0
    def is_sendable(self):
        return self.used < self.capcity
    def consumer2node(self):
        # to add lock
        self.used+=1
    def node2consumer(self):
        # to add lock
        self.used-=1

class Node():
    def __init__(self, link:Link, cache_size=100):
        self.link = link
        self.cache = [-1 for i in range(cache_size)]
    
    def cached_size(self):
        ans = 0
        for content_id in self.cache:
            if content_id != -1:
                ans += 1
        return ans
    
    def update_cache(self, content_id):
        """[summary]

        Args:
            content_id ([type]): [description]

        Returns:
            int: 1: in cache, 0 not in cache
        """
        if content_id in self.cache:
            index = self.cache.index(content_id)
            self.cache = [content_id] + self.cache[:index] + self.cache[index+1:]
            in_cache=True
        else:
            self.cache = [content_id] + self.cache[:-1]
            in_cache=False
        
        output(f"Node: in_cache={in_cache}, cache updated: {self.cache}")
        return in_cache
        
    def recv(self, packet:Packet):
        output(f"Node: packet received from consumer: {packet}")
        self.link.consumer2node()
        in_cache = self.update_cache(packet.content_id)
        packet.set_cache_states(in_cache)
        import copy
        global record
        record.append((packet, copy.deepcopy(self.cache)))
        flight_time = IN_CACHE_TIME if in_cache else OUT_CACHE_TIME
        return flight_time
        
    def send(self, packet:Packet):
        output(f"Node: packet send back to consumer: {packet}")
        packet.set_recv_timestamp()
        self.link.node2consumer()

        

class Consumer():
    def __init__(self, nodes:List[Node], consumer_id:int, consumer_type:int, send_interval:float, content_size=100 ):
        # consumer_type 没用上
        self.nodes:List[Node] = nodes
        self.consumer_id = consumer_id
        self.consumer_type = consumer_type
        self.content_size = content_size
        self.send_interval = send_interval
    
    def generate_packet(self):
        if self.consumer_id > 0:
            # 正常用户
            content_id = zipf(ZF_A, 1)[0]
        else:
            # 不正常用户
            content_id = randint(100, 160)
        return Packet(content_id, self.consumer_id, self.consumer_type)

    def get_node(self, packet:Packet):
        """
        while True:
            find all nodes: is_sendable
            if nodes is []:
                sleep
                continue
            for node in nodes:
                if node cache packet: return node
            return nodes[0]
        """

        """
        1. packet.content_id in cache: return node(sendable)
        2. packet

        """
        while True:
            sendable_nodes = []
            for node in self.nodes:
                if node.link.is_sendable():
                    sendable_nodes.append(node)
            if not sendable_nodes:
                time.sleep(self.send_interval)
                continue
            for node in sendable_nodes:
                if packet.content_id in node.cache:
                    return node
            ans = sendable_nodes[0]
            nodes_with_minimum_cached_size = []
            current_cached_size = 99999999999999
            for node in sendable_nodes:
                if node.cached_size() < current_cached_size:
                    nodes_with_minimum_cached_size = [node]
                    current_cached_size = node.cached_size()
                elif node.cached_size() == current_cached_size:
                    nodes_with_minimum_cached_size.append(node)
                else:
                    pass
            import random
            return random.choice(nodes_with_minimum_cached_size)

    def send(self):
        def call_recv(flight_time, packet):
            time.sleep(flight_time)
            node = packet.node
            node.send(packet)
            output(f"Consumer[{self.consumer_id}]: recv packet back from node: {packet}")
        
        packet = self.generate_packet()
        node = self.get_node(packet)
        output(f"get node with id={self.nodes.index(node)}, cache: {node.cache}, in_cache: {packet.content_id in node.cache}, sendable: {node.link.is_sendable()}, link used: {node.link.used}/{node.link.capcity}")
        packet.set_node(node)
        output(f"Consumer[{self.consumer_id}]: send packet to node: {packet}")
        flight_time = node.recv(packet)
        t = threading.Thread(target=call_recv, args=(flight_time, packet))
        t.setDaemon(True)
        t.start()
        
    def run(self):
        while True:
            if time.time() - GLOBAL_START > SIMULATION_TIME: return 
            time.sleep(self.send_interval) # 发包间隔
            self.send()
        
            
if __name__ == "__main__":
    link = Link(800)
    # link_capctity_list = [200,300,400,500,600]
    link_capctity_list = [50, 50, 50, 50, 50]
    node_cache_capcity_list = [10,10,10,10,10]
    num_node = 5

    assert(len(node_cache_capcity_list)==num_node)
    assert(len(link_capctity_list)==num_node)

    nodes = []
    for i in range(num_node):
        link = Link(link_capctity_list[i])
        node = Node(link,node_cache_capcity_list[i])
        nodes.append(node)
    num_normal_consumer = 10
    num_evil_consumer = 1
    normal_consumers = []
    evil_consumers = []
    for i in range(num_normal_consumer):
        consumer_id = i+1
        normal_consumers.append(Consumer(nodes, consumer_id, 0, NORMAL_SEND_INTERVAL, 100))
    for i in range(num_evil_consumer):
        consumer_id = -i-1
        evil_consumers.append(Consumer(nodes, consumer_id, 0, NORMAL_SEND_INTERVAL, 100))

    threads = []
    for consumer in normal_consumers:
        threads.append(threading.Thread(target=consumer.run))
    for consumer in evil_consumers:
        threads.append(threading.Thread(target=consumer.run))
    for t in threads:
        t.setDaemon(True)
        t.start()
    
    
    for t in threads:
        t.join()

    # for packet in node.record:
    #     # print(f"{packet}")
    #     print(f"{packet.send_timestamp-GLOBAL_START}\t{packet.content_id}\t{packet.consumer_id}\t{packet.in_cache}")
    
    node2id = dict()
    for i,node in enumerate(nodes):
        node2id[node] = i

    with open("result.csv", 'w') as f:
        f.write(f"time, content_id, comsumer_id, hit, node_id\n")
        for packet, cache in record:
            f.write(f"{packet.send_timestamp-GLOBAL_START},{packet.content_id},{packet.consumer_id},{packet.in_cache},{node2id[packet.node]}\n")
    
    for i in range(len(nodes)):
        with open(f"result_node{i}.csv", 'w') as f:
            f.write(f"time, content_id, comsumer_id, hit, node_id\n")
            for packet, cache in record:
                id = node2id[packet.node]
                if id != i:
                    continue
                f.write(f"{packet.send_timestamp-GLOBAL_START},{packet.content_id},{packet.consumer_id},{packet.in_cache},{node2id[packet.node]}\n")
            
        
    